babel 最开始的名字叫 6to5,主要是做 es6 到 es5 语法的转换和 polyfill,后来在 4.0 时改名为了 babel。虽然从 6to5 改名到了 babel,但是做的事情并没有变,依然是从高版本语法和 api 转换成低版本的语法并自动 polyfill 缺少的 api。

babel 是怎么实现这些功能的呢?如果让我们基于插件机制实现 babel 内置的转换功能,该会如何设计呢?

# 从插件到 preset

# 映射关系

要实现转换,第一步要明确转换什么: 划定一个集合放要转换的特性,再划定一个集合放转换到的目标特性,两者建立一一映射关系。就确定了我们要做哪些转换。

img

# exponentiation operator

比如乘方运算符,我们会用 Math.pow 来实现

let x = 10 ** 2;

x **= 3;

转换为

let x = Math.pow(10, 2);

x = Math.pow(x, 3);

img

# class

再比如 class,我们会用 function、prototype 来实现

class Test {
  constructor(name) {
    this.name = name;
  }

  logger() {
    console.log("Hello", this.name);
  }
}

转换为

function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}

var Test = (function() {
  function Test(name) {
    _classCallCheck(this, Test);

    this.name = name;
  }

  Test.prototype.logger = function logger() {
    console.log("Hello", this.name);
  };

  return Test;
})();

img

每一个语法都可以这样转换为低版本的语法,根据图灵完备的语言的特点(图灵完备的语言可以描述任何可计算问题),理论上所有的语法都可以用低版本的语法来实现。

但是只是转换并不能解决所有问题,涉及到某个对象的 api,比如 Array.prototype.find,这种 api 的兼容并不是需要转换语法,而是要在环境中注入我们实现的 api,也就是 polyfill (垫片)。

通过 syntax transform + api polyfill,我们就能在目标环境用高版本 javascript 的语法和 api。

# 需要转换的语言特性

babel 插件需要转换的特性包括 es 标准、proposal,还有 react、flow、typescript 等

# es spec

我们知道,TC39 (opens new window) 是制定 javascript 语言标准的组织,每年都会公布加入到语言标准的特性,es2015 (opens new window)、es2016、es2017 等。这些是我们要转换的语言特性范围。

img

在 babel6 时,分别用 preset-es2015、 preset-es2016 等来维护相应的 transform plugin,但在 babel7 的时候就改为 preset env 了。

# proposal

babel 要转换的不只是加入标准的特性,语言特性从提出到标准会有一个过程,分为几个阶段 (opens new window)。

  • 阶段 0 - Strawman: 只是一个想法,可能用 babel plugin 实现
  • 阶段 1 - Proposal: 值得继续的建议
  • 阶段 2 - Draft: 建立 spec
  • 阶段 3 - Candidate: 完成 spec 并且在浏览器实现
  • 阶段 4 - Finished: 会加入到下一年的 es20xx spec

这些还未加入到语言标准的特性也是要支持的。

babel6 的时候有 babel-state-x 的 preset,分别放不同阶段的特性,但是这个preset是变化的(每年都在变),比较难以维护,所以 babel7 以后就改成 plugin-proposal-xxx 了。

img

# react、flow、typescript

只是转换 javascript 本身的 es spec 和 proposal 的特性特性并不够,现在我们开发的时候 jsx、typescript、flow 这些都是会用的,babel 肯定也得支持。

这些转换对应的 plugin 分别放在不同 preset 里: preset-jsx、preset-typescript、preset-flow。

我们要转换的范围又大了一些。

img

# 插件分类

上面是插件要转换的语言特性,babel7 内置的实现这些特性的插件分为 syntax、transform、proposal 3类。

# syntax plugin

syntax plugin 只是在 parserOptions 中放入一个 flag 让 parser 知道要 parse 什么语法,最终的 parse 逻辑还是 babel parser(babylon) 实现的。

一般 syntax plugin 都是这样的:

import { declare } from "@babel/helper-plugin-utils";

export default declare(api => {
  api.assertVersion(7);

  return {
    name: "syntax-function-bind",

    manipulateOptions(opts, parserOpts) {
      parserOpts.plugins.push("functionBind");
    },
  };
});

这些插件的目的就是让 parser 能够正确的解析对应的语法成 AST。

# transform plugin

transform plugin 是对 AST 的转换,各种 es20xx 语言特性、typescript、jsx 等的转换都是在 transform plugin 里面实现的。

有的时候需要结合 syntax plugin 和 transform plugin, 比如 typescript 的语法解析要使用 @babel/plugin-syntax-typescript 在 parserOptions 放入解析 typescript 语法的选项,然后使用 @babel/plugin-transform-typescript 来转换解析出的 typescript 对应的 AST 的转换。但平时我们一般使用 @babel/preset-typescript,它对上面两个插件做了封装。

# proposal plugin

未加入语言标准的特性的 AST 转换插件叫 proposal plugin,其实他也是 transform plugin,但是为了和标准特性区分,所以这样叫。

完成 proposal 特性的支持,有时同样需要 综合 syntax plugin 和 proposal plugin,比如 function bind (:: 操作符)就需要同时使用 @babel/plugin-syntax-function-bind 和 @babel/plugin-proposal-function-bind。

总之,babel 的 plugin,就 @babel/plugin-syntax-xxx, @babel/plugin-transform-xxx、@babel/plugin-proposal-xxx 3种。

# preset

babel 的插件有很多,用于不同的目的需要不同的插件:

  • 不同版本的语言标准支持: preset-es2015、preset-es2016 等,babel7 后用 preset-env 代替
  • 未加入标准的语言特性的支持: 用于 stage0、stage1、stage2 的特性,babel7 后单独引入 proposal plugin
  • 用于 react、jsx、flow 的支持:分别封装相应的插件为 preset-react、preset-jsx、preset-flow,直接使用对应 preset 即可

# helper

每个特性的实现用一个 babel 插件实现,当 babel 插件多了,自然会有一些共同的逻辑。这部分逻辑怎么共享呢?

babel 设计了插件之间共享逻辑的机制,就是 helper。

helper 分为两种:

  • 一种是注入到 AST 的运行时用的全局函数
  • 一种是操作 AST 的工具函数,比如变量提升这种通用逻辑
# 注入到 AST 的全局函数

注入到 AST 的运行时用的全局函数,比如

class Guang {}

会被转换成

function _classCallCheck(instance, Constructor) {
  //...
}

var Guang = function Guang() {
  _classCallCheck(this, Guang);
};

这里的 _classCallCheck 就是 helper 注入的。

这类 helper 数量比较多,babel7 有 80 多个,都在 @babel/helpers 里面。在插件里使用的话,直接调用 this.addHelper,会在顶层作用域声明对应的 helper,然后返回对应的 identifier。

var transformObjectSetPrototypeOfToAssign = declare(function (api) {
    api.assertVersion(7);
    return {
      name: "transform-object-set-prototype-of-to-assign",
      visitor: {
        CallExpression: function CallExpression(path) {
          if (path.get("callee").matchesPattern("Object.setPrototypeOf")) {
            path.node.callee = this.addHelper("defaults");
          }
        }
      }
    };
});

这种 helper 是用于用低版本特性实现高版本特性的,比如用 function 和 prototype 实现 class,内部实现方式是通过 babel template 的方式。

img

你可能会发现这些函数在 babel runtime 里面也有同样的一份,没错,确实是一样的代码放在了两个位置。为什么会这样呢? 因为一个是在编译期间注入到 AST 的,而另一个是在运行时 require 的,所以虽然都是转换用的工具函数,但要写两份。(后者是通过 @babel/plugin-transform-runtime 转换之后在运行时引入的)

这是 @babel/runtime 包下的相同的 api,区别是 helper 是在编译时注入 AST,runtime 会随代码一起被打包,在运行时被引入。

image.png

# 操作 AST 的工具函数

操作 AST 的工具函数,比如

const hoistVariables = require('@babel/helper-hoist-variables').default;

cosnt plugin = function () {
    visitor: {
        VariableDeclaration(path) {
            hoistVariables(path.parentPath, (id) => {
                path.scope.parent.push({
                    id: path.scope.generateUidIdentifier(id.name)
                });
                return id;
            }, 'const' );
        }
    }
}

当输入为

function func(){
    const a = 1;
    const b = 2;
}

输出为

var _a, _b;

function func() {
  a = 1;
  b = 2;
}

我们借助 @babel/helper-hoist-variables 轻松实现了变量提升的逻辑。

再举一个例子

const importModule = require('@babel/helper-module-imports');

cosnt plugin = function ({ template }) {
    visitor: {
        Program(path) {
            const reactIdentifier = importModule.addDefault(path, 'lodash',{
                nameHint: '_'
            });
            path.node.body.push(template.ast(`const get = _.get`));
        }
    }
}

会在代码中加入模块引入和变量声明的代码

var _ = _interopRequireDefault(require("lodash")).default;

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

const get = _.get;

我们借助 @babel/helper-module-imports 可以很轻松的引入一个模块,通过 named import、default import 或者 namespace import 的方式。

这类 helper 的特点是需要手动引入对应的包,调用 api,而不是直接 this.addHelper 就行。

总之,babel helpers 是用于 babel plugin 逻辑复用的一些工具函数,分为用于注入 runtime 代码的 helper 和用于简化 AST 操作 的 helper两种。第一种都在 @babel/helpers 包里,直接 this.addHelper(name) 就可以引入, 而第二种需要手动引入包和调用 api。

# babel runtime

babel runtime 里面放运行时加载的模块,会被打包工具打包到产物中,下面放着各种需要在 runtime 使用的函数,包括三部分:regenerator、corejs、helper。

  • corejs 这就是新的 api 的 polyfill,分为 2 和 3 两个版本,3 才实现了实例方法的polyfill
  • regenerator 是 facebook 实现的 aync 的 runtime 库,babel 使用 regenerator-runtime (opens new window)来支持实现 async await 的支持。
  • helper 是 babel 做语法转换时用到的函数,比如 _typeof、_extends 等

babel 做语法转换和 api 的 polyfill,需要自己实现一部分 runtime 的函数,就是 helper 部分,有的也没有自己实现,用的第三方的,比如 regenerator 是用的 facebook 的。api 的 polyfill 也是用的 core-js 的,babel 对它们做了整合。

# 从目标语言版本到目标浏览器

我们通过插件完成了各种代码(es next、proposal、typescript/flow/jsx...)到 es5 的转换,然后把不同的转换插件封装到不同的 preset (preset-env、preset-typescript、preset-react...)里,而且还把插件内部的公共逻辑抽成 helper 来复用,并且提供了 runtime 包用于注入运行时的 api。这样已经能够达到不同语法的代码转 es5 同时对 api 进行 polyfill 的目标了。 但是这样就够了么?还有哪些问题?

  • 我们现在是指定了目标环境支持 es5,那么如果目标环境支持了部分 es6(es2015)、es7(es2016)等,那岂不是做了很多无用功?
  • preset-es2015、preset-es2016、preset-stage-x 这种 preset 跟随版本走的,那岂不是经常变,得经常改这些 preset 的内容 (当某个提案从 stage 0 进入到 stage 1 就得改下),这样多累啊,而且用户也得经常改配置,stage-x 对他来说是个黑盒,不靠谱。

怎么解决这些问题呢?

babel6 到 babel7 的变化给出了答案:

preset 经常变的问题通过废弃 stage-x 和 es20xx 的 preset,改成 preset-env 和 plugin-proposal-xx 的方式来解决, 这样不需要指定用的是 es 几了,默认会全部支持,包含所有的已经是语言标准特性的 transform plugin (opens new window), 而且 stage-x 有哪些不再是黑盒,用户想用啥还在 proposal 的特性直接显示引入对应的 plugin。

多转换了目标环境支持的语法或 api 的问题是要解决转换的精准度的问题,可是目标环境那么多,浏览器版本、node 版本、electron 版本每年都在变,怎么做到精准?

# comat-table

答案是 compat-table 的数据,compat-table 提供了每个特性在不同环境中的支持版本。

比如默认参数 (opens new window)这个 es2015 的特性,可以查到在 babel6 且 corejs2 以上支持,在 chrome 中是 49 以上支持,chrome48 中还是实验特性,在 node6 以上支持,等等。

img

光是这些数据还不够,electron 有自己的版本,要支持 electron 得需要 electron 版本和它用的 chromuim 的版本的对应关系。

万幸有 electron-to-chromium 这个项目,它维护了 electron 版本到 chromium 版本的映射关系 (opens new window)。

img

也可以反过来查询 chromium 版本在哪些 electron 版本中使用 (opens new window)。

img

有了这些数据,我们就能知道每一个特性在哪些环境的什么版本支持。

babel7 在 @babel/compat-data 这个包里面维护了这种特性到环境支持版本的映射关系,包括 plugin 实现的特性的版本支持情况 (opens new window)(包括 transform 和 proposal ),也包括 corejs 所 polyfill 的特性的版本支持情况 (opens new window)。

img

这样我们就知道每一个特性是在什么环境中支持的了,接下来只要用户指定一个环境,我们就能做到按需转换!

# browserslist

那开发者怎么指定环境呢?

让开发者写每个环境的版本是啥肯定不靠谱,这时候就要借助 browerslist 了,它提供了一个从 query (查询表达式) 到对应环境版本的转换。

比如我们可以通过 last 1 version 来查询最新的各环境的版本

img

也可以通过 supports es6-module 查询所有支持 es module 的环境版本

img

具体查询的语法有很多,可以去 browserslist 的 query 文档 (opens new window)中学习,这里就不展开了。

# @babel/preset-env

现在有了什么特性在什么环境版本中支持,有了可以通过 query 指定目标环境版本的工具,那么就可以上手改了,从都转成 es5 到根据目标环境确定不支持的特性,只转换这部分特性,这就是 @babel/preset-env 做的事情。

有了 @babel/compat-data 的数据,那么只要用户指定他的目标环境是啥就可以了,这时候可以用 browserslist 的 query 来写,比如 last 1 version, > 1% 这种字符串,babel 会使用 brwoserslist 来把它们转成目标环境具体版本的数据。

img

有了不同特性支持的环境的最低版本的数据,有了具体的版本,那么过滤出来的就是目标环境不支持的特性,然后引入它们对应的插件即可。这就是 preset-env 做的事情(按照目标环境按需引入插件)。

img

配置方式比如:

{
    "presets": [["@babel/preset-env", { "targets": "> 0.25%, not dead" }]]
}

这样就通过 preset-env 解决了多转换了目标环境已经支持的特性的问题。

其实 polyfill 也可以通过 targets 来过滤。

img

不再手动引入 polyfill,那么怎么引入? 当然是用 preset-env 自动引入了。但是不是默认就会启用这个功能,需要配置。

{
    "presets": [["@babel/preset-env", {
        "targets": "> 0.25%, not dead",
        "useBuiltIns": "usage",// or "entry" or "false"
        "corejs": 3
    }]]
}

配置下 corejs 和 useBuiltIns。

  • corejs 就是 babel 7 所用的 polyfill,需要指定下版本,corejs 3 才支持实例方法(比如 Array.prototype.fill )的 polyfill。
  • useBuiltIns 就是使用 polyfill (corejs)的方式,是在入口处全部引入(entry),还是每个文件引入用到的(usage),或者不引入(false)。

配置了这两个 option 就可以自动引入 polyfill 了。

img

# @babel/preset-env 的配置

这个包的配置比较多,首先我们要指定的是 targets,也就是 browserslist 的 query,这个同样可以在 .browserslistrc 的配置文件中指定(别的工具也可能用到)。

# targets

targets 配啥呢?

可以配 query 或者直接指定环境版本(query 的结果也是环境版本)。

环境有这些:

chrome, opera, edge, firefox, safari, ie, ios, android, node, electron

可以指定 query:

{
  "targets": "> 0.25%, not dead"
}

也可以直接指定环境版本;

{
  "targets": {
    "chrome": "58",
    "ie": "11"
  }
}

要指定支持 es module 的环境,可以使用 query,也可以使用 esmodules option。

{
    "targets": {
        "esmodules": true
    }
}
# include & exclude

通过 targets 的指定,babel 会自动引入一些插件 (opens new window),但当需要手动指定要 include 或者 exclude 什么插件的时候可以使用这个 option。

不过这个只是针对 transform plugin,对于 proposal plugin,要在 plugins的 option 单独引入。

一般情况下并不需要单独 include 啥,用 babel 根据 targets 自动引入的 transform plugin 即可,只是有一些特殊场景下可能会用到。

# modules

targets 是指定目标环境,modules 是指定目标模块规范,取值有 amd、umd、systemjs、commonjs (cjs)、auto、false。

  • amd、umd、systemjs、commonjs (cjs) 这四个分别指定不同的目标模块规范
  • false 是不转换模块规范
  • auto 则是自动探测,默认值也是这个。

其实一般这个 option 都是 bundler 来设置的,因为 bundler 负责模块转换,自然知道要转换成什么模块规范。我们平时就用默认值 auto 即可。

类似 babel parser 可以设置 moduleType 为 unambiguous,让 babel 根据是否包含 import / export 语法来自动设置为具体的值。 这个 auto 也是一样,会根据探测到的目标环境支持的模块规范来做转换。依据是在 transform 的时候传入的 caller 数据。

babel.transformFileSync("example.js", {
  caller: {
    name: "my-custom-tool",
    supportsStaticESM: true,
  },
});

比如在调用 transformFile 的 api 的时候传入了 caller 是支持 esm 的,那么在 targets 的 modules 就会自动设置为 esm。

# debug

debug 这个参数配置比较简单,就是 true 或者 false,但是意义却很大。我们知道 babel 会根据 targets 支持的特性来过滤 transform plugins 和 polyfills(core-js)。想知道最终使用的 transform plugin 和引入的 core-js 模块是否对,那就可以把 debug 设为 true,这样在控制台打印这些数据。

比如

const sourceCode = `
  import "core-js";
  new Array(5).fill('111');
`;

const { code, map } = babel.transformSync(sourceCode, {
    filename: 'a.mjs',
    targets: {
        browsers: 'Chrome 45',
    },
    presets: [
        ['@babel/env', {
            debug: true,
            useBuiltIns: 'usage',
            corejs: 3
        }]
    ]
});

设置 debug 为 true,会打印 targets 和根据 tragets 过滤出的的 plugin 和 preset:

@babel/preset-env: `DEBUG` option

Using targets:
{
  "chrome": "45"
}

Using modules transform: auto

Using plugins:
  proposal-numeric-separator { chrome < 75 }
  proposal-logical-assignment-operators { chrome < 85 }
  proposal-nullish-coalescing-operator { chrome < 80 }
  proposal-optional-chaining { chrome }
  proposal-json-strings { chrome < 66 }
  proposal-optional-catch-binding { chrome < 66 }
  transform-parameters { chrome < 49 }
  proposal-async-generator-functions { chrome < 63 }
  proposal-object-rest-spread { chrome < 60 }
  transform-dotall-regex { chrome < 62 }
  proposal-unicode-property-regex { chrome < 64 }
  transform-named-capturing-groups-regex { chrome < 64 }
  transform-async-to-generator { chrome < 55 }
  transform-exponentiation-operator { chrome < 52 }
  transform-function-name { chrome < 51 }
  transform-arrow-functions { chrome < 47 }
  transform-classes { chrome < 46 }
  transform-object-super { chrome < 46 }
  transform-for-of { chrome < 51 }
  transform-sticky-regex { chrome < 49 }
  transform-unicode-regex { chrome < 50 }
  transform-spread { chrome < 46 }
  transform-destructuring { chrome < 51 }
  transform-block-scoping { chrome < 49 }
  transform-new-target { chrome < 46 }
  transform-regenerator { chrome < 50 }
  proposal-export-namespace-from { chrome < 72 }
  transform-modules-commonjs
  proposal-dynamic-import
corejs3: `DEBUG` option

Using targets: {
  "chrome": "45"
}

Using polyfills with `usage-global` method:
regenerator: `DEBUG` option

Using targets: {
  "chrome": "45"
}

Using polyfills with `usage-global` method:

  When setting `useBuiltIns: 'usage'`, polyfills are automatically imported when needed.
  Please remove the direct import of `core-js` or use `useBuiltIns: 'entry'` instead.

[/Users/zhaixuguang/code/research/babel/a.mjs]
Based on your code and targets, the corejs3 polyfill did not add any polyfill.

[/Users/zhaixuguang/code/research/babel/a.mjs]
Based on your code and targets, the regenerator polyfill did not add any polyfill.

用到了哪些插件一目了然,开发时可以开启这个配置项。

更多配置项可以在文档中 (opens new window)查看。

# 从注入到抽离

preset-env 会在使用到新特性的地方注入 helper 到 AST 中,并且会引入用到的特性的 polyfill (corejs + regenerator),这样会导致两个问题:

  • 重复注入 helper 的实现,导致代码冗余
  • polyfill 污染全局环境

解决这两个问题的思路就是抽离出来,然后作为模块引入,这样多个模块复用同一份代码就不会冗余了,而且 polyfill 是模块化引入的也不会污染全局环境。

这个逻辑是在 @babel/plugin-transform-runtime 包里实现的。它可以把直接注入全局的方式改成模块化引入。

比如使用 preset-env 的时候是全局引入的:

img

当引入 @babel/plugin-transform-runtime 就可以模块化引入:

img

这样就不再污染全局环境了。

# babel7 的问题

我们先来试验一下:

看一下 Array.prototype.fill 的环境支持情况:

img

可以看到在 Chrome 45 及以上支持这个特性,而在 Chrome 44 就不支持了。

我们先单独试一下 preset-env:

当指定 targets 为 Chrome 44 时,应该自动引入polyfill:

img

当指定 targets 为 Chrome 45 时,不需要引入polyfill:

img

结果都符合预期,44 引入,45 不引入。

我们再来试试 @babel/plugin-transform-runtime:

img

是不是发现问题了,Chrome 45 不是支持 Array.prototype.fill 方法么,为啥还是引入了 polyfill。

因为 babel 中插件的应用顺序是:先 plugin 再 preset,plugin 从左到右,preset 从右到左,这样 plugin-transform-runtime 是在 preset-env 前面的。等 @babel/plugin-transform-runtime 转完了之后,再交给 preset-env 这时候已经做了无用的转换了。而 @babel/plugin-transform-runtime 并不支持 targets 的配置,就会做一些多余的转换和 polyfill。

这个问题在即将到来的 babel8 中得到了解决。

# babel8

babel8 提供了 一系列 babel polyfill 的包 (opens new window),解决了 babel7 的 @babel/plugin-transform-runtime 的遗留问题,可以通过 targets 来按需精准引入 polyfill。

babel8 支持配置一个 polyfill provider,也就是说你可以指定 corejs2、corejs3、es-shims 等 polyfill,还可以自定义 polyfil。

有了 polyfill 源之后,使用 polyfill 的方式也把之前 transform-runtime 做的事情内置了,从之前的 useBuiltIns: entry、 useBuiltIns: usage 的两种,变成了 3 种:

  • entry-global: 这个和之前的 useBuiltIns: entry 对标,就是全局引入 polyfill。

img

  • usage-entry: 这个和 useBuiltIns: usage 对标,就是具体模块引入用到的 polyfill。

img

  • usage-pure:这个就是之前需要 transform-runtime 插件做的事情,使用不污染全局变量的 pure 的方式引入具体模块用到的 polyfill.

img

其实这三种方式 babel 7 也支持,但是 babel8 不再需要 transform-runtime 插件了,而且还支持了 polyfill provider 的配置。

# 思路梳理

babel 的功能都是通过插件完成的,但是直接指定插件太过麻烦,所以设计出了 preset,我们学习 babel 的内置功能基本等价于学习 preset 的使用。主要是 preset-env、preset-typescript 这些。

但是一些 proposal 的插件需要单独引入,并且 @babel/plugin-transform-runtime也要单独引入。

学习内置功能的话 preset 是重点,但是最终完成功能的还是通过插件。小册的重点在于插件上,介绍内置的 preset 只是梳理下 babel 实现编译的思路。

# 总结

上一节我们基于 plugin 和 preset 已经能够完成 esnext 等代码转目标环境 js 代码的功能,但是还不完美。

这一节我们介绍了 @babel/preset-env,它基于每种特性的在不同环境的最低支持版本的数据和配置的 targets 来过滤插件,这样能减少很多没必要的转换和 polyfill。

如果希望把一些公共的 helper、core-js、regenerator 等注入的 runtime 函数抽离出来,并且以模块化的方式引入,那么需要用 @babel/plugin-transform-runtime 这个包。

@babel/plugin-transform-runtime 不支持根据 targets 的过滤,和 @babel/preset-env 配合时有问题,这个在 babel8 中得到了解决。babel8 提供了很多 babel polyfill 包,支持了 polyfill provider 的配置,而且还可以选择注入方式。不再需要 @babel/plugin-transform-runtime 插件了。

阅读全文